// -----------------------------------------------------------------------
// <copyright file="TriangleReader.cs" company="">
// Original Triangle code by Jonathan Richard Shewchuk, http://www.cs.cmu.edu/~quake/triangle.html
// Triangle.NET code by Christian Woltering, http://triangle.codeplex.com/
// </copyright>
// -----------------------------------------------------------------------

namespace UnityEngine.U2D.Animation.TriangleNet
    .IO
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using Animation.TriangleNet.Geometry;

    /// <summary>
    /// Helper methods for reading Triangle file formats.
    /// </summary>
    internal class TriangleReader
    {
        static NumberFormatInfo nfi = NumberFormatInfo.InvariantInfo;

        int startIndex = 0;

        #region Helper methods

        private bool TryReadLine(StreamReader reader, out string[] token)
        {
            token = null;

            if (reader.EndOfStream)
            {
                return false;
            }

            string line = reader.ReadLine().Trim();

            while (IsStringNullOrWhiteSpace(line) || line.StartsWith("#"))
            {
                if (reader.EndOfStream)
                {
                    return false;
                }

                line = reader.ReadLine().Trim();
            }

            token = line.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);

            return true;
        }

        /// <summary>
        /// Read vertex information of the given line.
        /// </summary>
        /// <param name="data">The input geometry.</param>
        /// <param name="index">The current vertex index.</param>
        /// <param name="line">The current line.</param>
        /// <param name="attributes">Number of point attributes</param>
        /// <param name="marks">Number of point markers (0 or 1)</param>
        private void ReadVertex(List<Vertex> data, int index, string[] line, int attributes, int marks)
        {
            double x = double.Parse(line[1], nfi);
            double y = double.Parse(line[2], nfi);

            var v = new Vertex(x, y);

            // Read a vertex marker.
            if (marks > 0 && line.Length > 3 + attributes)
            {
                v.Label = int.Parse(line[3 + attributes]);
            }

            if (attributes > 0)
            {
#if USE_ATTRIBS
                var attribs = new double[attributes];

                // Read the vertex attributes.
                for (int j = 0; j < attributes; j++)
                {
                    if (line.Length > 3 + j)
                    {
                        attribs[j] = double.Parse(line[3 + j], nfi);
                    }
                }

                v.attributes = attribs;
#endif
            }

            data.Add(v);
        }

        #endregion

        #region Main I/O methods

        /// <summary>
        /// Reads geometry information from .node or .poly files.
        /// </summary>
        public void Read(string filename, out Polygon polygon)
        {
            polygon = null;

            string path = Path.ChangeExtension(filename, ".poly");

            if (File.Exists(path))
            {
                polygon = ReadPolyFile(path);
            }
            else
            {
                path = Path.ChangeExtension(filename, ".node");
                polygon = ReadNodeFile(path);
            }
        }

        /// <summary>
        /// Reads a mesh from .node, .poly or .ele files.
        /// </summary>
        public void Read(string filename, out Polygon geometry, out List<ITriangle> triangles)
        {
            triangles = null;

            Read(filename, out geometry);

            string path = Path.ChangeExtension(filename, ".ele");

            if (File.Exists(path) && geometry != null)
            {
                triangles = ReadEleFile(path);
            }
        }

        /// <summary>
        /// Reads geometry information from .node or .poly files.
        /// </summary>
        public IPolygon Read(string filename)
        {
            Polygon geometry = null;

            Read(filename, out geometry);

            return geometry;
        }

        #endregion

        /// <summary>
        /// Read the vertices from a file, which may be a .node or .poly file.
        /// </summary>
        /// <param name="nodefilename"></param>
        /// <remarks>Will NOT read associated .ele by default.</remarks>
        public Polygon ReadNodeFile(string nodefilename)
        {
            return ReadNodeFile(nodefilename, false);
        }

        /// <summary>
        /// Read the vertices from a file, which may be a .node or .poly file.
        /// </summary>
        /// <param name="nodefilename"></param>
        /// <param name="readElements"></param>
        public Polygon ReadNodeFile(string nodefilename, bool readElements)
        {
            Polygon data;

            startIndex = 0;

            string[] line;
            int invertices = 0, attributes = 0, nodemarkers = 0;

            using (var reader = new StreamReader(nodefilename))
            {
                if (!TryReadLine(reader, out line))
                {
                    throw new Exception("Can't read input file.");
                }

                // Read number of vertices, number of dimensions, number of vertex
                // attributes, and number of boundary markers.
                invertices = int.Parse(line[0]);

                if (invertices < 3)
                {
                    throw new Exception("Input must have at least three input vertices.");
                }

                if (line.Length > 1)
                {
                    if (int.Parse(line[1]) != 2)
                    {
                        throw new Exception("Triangle only works with two-dimensional meshes.");
                    }
                }

                if (line.Length > 2)
                {
                    attributes = int.Parse(line[2]);
                }

                if (line.Length > 3)
                {
                    nodemarkers = int.Parse(line[3]);
                }

                data = new Polygon(invertices);

                // Read the vertices.
                if (invertices > 0)
                {
                    for (int i = 0; i < invertices; i++)
                    {
                        if (!TryReadLine(reader, out line))
                        {
                            throw new Exception("Can't read input file (vertices).");
                        }

                        if (line.Length < 3)
                        {
                            throw new Exception("Invalid vertex.");
                        }

                        if (i == 0)
                        {
                            startIndex = int.Parse(line[0], nfi);
                        }

                        ReadVertex(data.Points, i, line, attributes, nodemarkers);
                    }
                }
            }

            if (readElements)
            {
                // Read area file
                string elefile = Path.ChangeExtension(nodefilename, ".ele");
                if (File.Exists(elefile))
                {
                    ReadEleFile(elefile, true);
                }
            }

            return data;
        }

        /// <summary>
        /// Read the vertices and segments from a .poly file.
        /// </summary>
        /// <param name="polyfilename"></param>
        /// <remarks>Will NOT read associated .ele by default.</remarks>
        public Polygon ReadPolyFile(string polyfilename)
        {
            return ReadPolyFile(polyfilename, false, false);
        }

        /// <summary>
        /// Read the vertices and segments from a .poly file.
        /// </summary>
        /// <param name="polyfilename"></param>
        /// <param name="readElements">If true, look for an associated .ele file.</param>
        /// <remarks>Will NOT read associated .area by default.</remarks>
        public Polygon ReadPolyFile(string polyfilename, bool readElements)
        {
            return ReadPolyFile(polyfilename, readElements, false);
        }

        /// <summary>
        /// Read the vertices and segments from a .poly file.
        /// </summary>
        /// <param name="polyfilename"></param>
        /// <param name="readElements">If true, look for an associated .ele file.</param>
        /// <param name="readElements">If true, look for an associated .area file.</param>
        public Polygon ReadPolyFile(string polyfilename, bool readElements, bool readArea)
        {
            // Read poly file
            Polygon data;

            startIndex = 0;

            string[] line;
            int invertices = 0, attributes = 0, nodemarkers = 0;

            using (var reader = new StreamReader(polyfilename))
            {
                if (!TryReadLine(reader, out line))
                {
                    throw new Exception("Can't read input file.");
                }

                // Read number of vertices, number of dimensions, number of vertex
                // attributes, and number of boundary markers.
                invertices = int.Parse(line[0]);

                if (line.Length > 1)
                {
                    if (int.Parse(line[1]) != 2)
                    {
                        throw new Exception("Triangle only works with two-dimensional meshes.");
                    }
                }

                if (line.Length > 2)
                {
                    attributes = int.Parse(line[2]);
                }

                if (line.Length > 3)
                {
                    nodemarkers = int.Parse(line[3]);
                }

                // Read the vertices.
                if (invertices > 0)
                {
                    data = new Polygon(invertices);

                    for (int i = 0; i < invertices; i++)
                    {
                        if (!TryReadLine(reader, out line))
                        {
                            throw new Exception("Can't read input file (vertices).");
                        }

                        if (line.Length < 3)
                        {
                            throw new Exception("Invalid vertex.");
                        }

                        if (i == 0)
                        {
                            // Set the start index!
                            startIndex = int.Parse(line[0], nfi);
                        }

                        ReadVertex(data.Points, i, line, attributes, nodemarkers);
                    }
                }
                else
                {
                    // If the .poly file claims there are zero vertices, that means that
                    // the vertices should be read from a separate .node file.
                    data = ReadNodeFile(Path.ChangeExtension(polyfilename, ".node"));

                    invertices = data.Points.Count;
                }

                var points = data.Points;

                if (points.Count == 0)
                {
                    throw new Exception("No nodes available.");
                }

                // Read the segments from a .poly file.

                // Read number of segments and number of boundary markers.
                if (!TryReadLine(reader, out line))
                {
                    throw new Exception("Can't read input file (segments).");
                }

                int insegments = int.Parse(line[0]);

                int segmentmarkers = 0;
                if (line.Length > 1)
                {
                    segmentmarkers = int.Parse(line[1]);
                }

                int end1, end2, mark;
                // Read and insert the segments.
                for (int i = 0; i < insegments; i++)
                {
                    if (!TryReadLine(reader, out line))
                    {
                        throw new Exception("Can't read input file (segments).");
                    }

                    if (line.Length < 3)
                    {
                        throw new Exception("Segment has no endpoints.");
                    }

                    // TODO: startIndex ok?
                    end1 = int.Parse(line[1]) - startIndex;
                    end2 = int.Parse(line[2]) - startIndex;
                    mark = 0;

                    if (segmentmarkers > 0 && line.Length > 3)
                    {
                        mark = int.Parse(line[3]);
                    }

                    if ((end1 < 0) || (end1 >= invertices))
                    {
                        if (Log.Verbose)
                        {
                            Log.Instance.Warning("Invalid first endpoint of segment.",
                                "MeshReader.ReadPolyfile()");
                        }
                    }
                    else if ((end2 < 0) || (end2 >= invertices))
                    {
                        if (Log.Verbose)
                        {
                            Log.Instance.Warning("Invalid second endpoint of segment.",
                                "MeshReader.ReadPolyfile()");
                        }
                    }
                    else
                    {
                        data.Add(new Segment(points[end1], points[end2], mark));
                    }
                }

                // Read holes from a .poly file.

                // Read the holes.
                if (!TryReadLine(reader, out line))
                {
                    throw new Exception("Can't read input file (holes).");
                }

                int holes = int.Parse(line[0]);
                if (holes > 0)
                {
                    for (int i = 0; i < holes; i++)
                    {
                        if (!TryReadLine(reader, out line))
                        {
                            throw new Exception("Can't read input file (holes).");
                        }

                        if (line.Length < 3)
                        {
                            throw new Exception("Invalid hole.");
                        }

                        data.Holes.Add(new Point(double.Parse(line[1], nfi),
                            double.Parse(line[2], nfi)));
                    }
                }

                // Read area constraints (optional).
                if (TryReadLine(reader, out line))
                {
                    int id, regions = int.Parse(line[0]);

                    if (regions > 0)
                    {
                        for (int i = 0; i < regions; i++)
                        {
                            if (!TryReadLine(reader, out line))
                            {
                                throw new Exception("Can't read input file (region).");
                            }

                            if (line.Length < 4)
                            {
                                throw new Exception("Invalid region attributes.");
                            }

                            if (!int.TryParse(line[3], out id))
                            {
                                id = i;
                            }

                            double area = 0.0;

                            if (line.Length > 4)
                            {
                                double.TryParse(line[4], NumberStyles.Number, nfi, out area);
                            }

                            // Triangle's .poly file format allows region definitions with
                            // either 4 or 5 parameters, and different interpretations for
                            // them depending on the number of parameters.
                            //
                            // See http://www.cs.cmu.edu/~quake/triangle.poly.html
                            //
                            // The .NET version will interpret the fourth parameter always
                            // as an integer region id and the optional fifth parameter as
                            // an area constraint.

                            data.Regions.Add(new RegionPointer(
                                double.Parse(line[1], nfi),     // Region x
                                double.Parse(line[2], nfi),     // Region y
                                id, area));
                        }
                    }
                }
            }

            // Read ele file
            if (readElements)
            {
                string elefile = Path.ChangeExtension(polyfilename, ".ele");
                if (File.Exists(elefile))
                {
                    ReadEleFile(elefile, readArea);
                }
            }

            return data;
        }

        /// <summary>
        /// Read elements from an .ele file.
        /// </summary>
        /// <param name="elefilename">The file name.</param>
        /// <returns>A list of triangles.</returns>
        public List<ITriangle> ReadEleFile(string elefilename)
        {
            return ReadEleFile(elefilename, false);
        }

        /// <summary>
        /// Read the elements from an .ele file.
        /// </summary>
        /// <param name="elefilename"></param>
        /// <param name="data"></param>
        /// <param name="readArea"></param>
        private List<ITriangle> ReadEleFile(string elefilename, bool readArea)
        {
            int intriangles = 0, attributes = 0;

            List<ITriangle> triangles;

            using (var reader = new StreamReader(elefilename))
            {
                // Read number of elements and number of attributes.
                string[] line;
                bool validRegion = false;

                if (!TryReadLine(reader, out line))
                {
                    throw new Exception("Can't read input file (elements).");
                }

                intriangles = int.Parse(line[0]);

                // We irgnore index 1 (number of nodes per triangle)
                attributes = 0;
                if (line.Length > 2)
                {
                    attributes = int.Parse(line[2]);
                    validRegion = true;
                }

                if (attributes > 1)
                {
                    Log.Instance.Warning("Triangle attributes not supported.", "FileReader.Read");
                }

                triangles = new List<ITriangle>(intriangles);

                InputTriangle tri;

                // Read triangles.
                for (int i = 0; i < intriangles; i++)
                {
                    if (!TryReadLine(reader, out line))
                    {
                        throw new Exception("Can't read input file (elements).");
                    }

                    if (line.Length < 4)
                    {
                        throw new Exception("Triangle has no nodes.");
                    }

                    // TODO: startIndex ok?
                    tri = new InputTriangle(
                        int.Parse(line[1]) - startIndex,
                        int.Parse(line[2]) - startIndex,
                        int.Parse(line[3]) - startIndex);

                    // Read triangle region
                    if (attributes > 0 && validRegion)
                    {
                        int region = 0;
                        validRegion = int.TryParse(line[4], out region);
                        tri.label = region;
                    }

                    triangles.Add(tri);
                }
            }

            // Read area file
            if (readArea)
            {
                string areafile = Path.ChangeExtension(elefilename, ".area");
                if (File.Exists(areafile))
                {
                    ReadAreaFile(areafile, intriangles);
                }
            }

            return triangles;
        }

        /// <summary>
        /// Read the area constraints from an .area file.
        /// </summary>
        /// <param name="areafilename"></param>
        /// <param name="intriangles"></param>
        /// <param name="data"></param>
        private double[] ReadAreaFile(string areafilename, int intriangles)
        {
            double[] data = null;

            using (var reader = new StreamReader(areafilename))
            {
                string[] line;

                if (!TryReadLine(reader, out line))
                {
                    throw new Exception("Can't read input file (area).");
                }

                if (int.Parse(line[0]) != intriangles)
                {
                    Log.Instance.Warning("Number of area constraints doesn't match number of triangles.",
                        "ReadAreaFile()");
                    return null;
                }

                data = new double[intriangles];

                // Read area constraints.
                for (int i = 0; i < intriangles; i++)
                {
                    if (!TryReadLine(reader, out line))
                    {
                        throw new Exception("Can't read input file (area).");
                    }

                    if (line.Length != 2)
                    {
                        throw new Exception("Triangle has no nodes.");
                    }

                    data[i] = double.Parse(line[1], nfi);
                }
            }

            return data;
        }

        /// <summary>
        /// Read an .edge file.
        /// </summary>
        /// <param name="edgeFile">The file name.</param>
        /// <param name="invertices">The number of input vertices (read from a .node or .poly file).</param>
        /// <returns>A List of edges.</returns>
        public List<Edge> ReadEdgeFile(string edgeFile, int invertices)
        {
            // Read poly file
            List<Edge> data = null;

            startIndex = 0;

            string[] line;

            using (var reader = new StreamReader(edgeFile))
            {
                // Read the edges from a .edge file.

                // Read number of segments and number of boundary markers.
                if (!TryReadLine(reader, out line))
                {
                    throw new Exception("Can't read input file (segments).");
                }

                int inedges = int.Parse(line[0]);

                int edgemarkers = 0;
                if (line.Length > 1)
                {
                    edgemarkers = int.Parse(line[1]);
                }

                if (inedges > 0)
                {
                    data = new List<Edge>(inedges);
                }

                int end1, end2, mark;
                // Read and insert the segments.
                for (int i = 0; i < inedges; i++)
                {
                    if (!TryReadLine(reader, out line))
                    {
                        throw new Exception("Can't read input file (segments).");
                    }

                    if (line.Length < 3)
                    {
                        throw new Exception("Segment has no endpoints.");
                    }

                    // TODO: startIndex ok?
                    end1 = int.Parse(line[1]) - startIndex;
                    end2 = int.Parse(line[2]) - startIndex;
                    mark = 0;

                    if (edgemarkers > 0 && line.Length > 3)
                    {
                        mark = int.Parse(line[3]);
                    }

                    if ((end1 < 0) || (end1 >= invertices))
                    {
                        if (Log.Verbose)
                        {
                            Log.Instance.Warning("Invalid first endpoint of segment.",
                                "MeshReader.ReadPolyfile()");
                        }
                    }
                    else if ((end2 < 0) || (end2 >= invertices))
                    {
                        if (Log.Verbose)
                        {
                            Log.Instance.Warning("Invalid second endpoint of segment.",
                                "MeshReader.ReadPolyfile()");
                        }
                    }
                    else
                    {
                        data.Add(new Edge(end1, end2, mark));
                    }
                }
            }

            return data;
        }

        bool IsStringNullOrWhiteSpace(string value)
        {
            if (value != null)
            {
                for (int i = 0; i < value.Length; i++)
                {
                    if (!char.IsWhiteSpace(value[i]))
                    {
                        return false;
                    }
                }
            }
            return true;
        }
    }
}
